//
//  SocketIOClient.m
//  Socket.IO-Client
//
//  Created by alanwang.dev on 16/8/8.
//  Copyright © 2016年 com.alanwang.dev. All rights reserved.
//

#import "SocketIOClient.h"
#import "SocketPacket.h"
#import "SocketAckManager.h"
#import "SocketLogger.h"
#import "SocketEventHandler.h"
#import "SocketEngineSpec.h"

typedef void(^Handler)(SocketAnyEvent *);

@interface SocketIOClient()

@property (nonatomic) SocketEngineSpec *engine;
@property (nonatomic) SocketIOClientStatus status;

@end


@implementation SocketIOClient{
    dispatch_queue_t emitQueue;
    NSString *logType;
    dispatch_queue_t parseQueue;
    Handler anyHandler;
    NSInteger currentReconnectAttempt;
    NSMutableArray<SocketEventHandler *> *handlers;
    SocketAckManager *ackHandlers;
    BOOL reconnecting;
    
    NSInteger currentAck;
    dispatch_queue_t handleQueue;
    NSInteger reconnectAttempts;
    
    NSMutableArray<SocketPacket *> *waitingPackets;
}

- (instancetype)initWithSocketURL:(NSURL *)socketURL options:(NSDictionary *)options{
    if (self = [super init]) {
        // 初始化列表
        self.forceNew = false;
        self.nsp = @"/";
        self.reconnects = YES;
        self.reconnectWait = 10;
        
        emitQueue = dispatch_queue_create("com.socketio.emitQueue", DISPATCH_QUEUE_SERIAL);
        logType = @"SocketIOClient";
        parseQueue = dispatch_queue_create("com.socketio.parseQueue", DISPATCH_QUEUE_SERIAL);
        currentReconnectAttempt = 0;
        handlers = [NSMutableArray array];
        ackHandlers = [[SocketAckManager alloc] init];
        reconnecting = NO;
        
        currentAck = -1;
        handleQueue = dispatch_get_main_queue();
        reconnectAttempts = -1;
        
        waitingPackets = [NSMutableArray array];
        
        // 赋值
        self.options = options.mutableCopy;
        self.socketURL = socketURL;
        
        if ([socketURL.absoluteString hasPrefix:@"https://"]) {
            //            [self.options insertIgnore:YES forKey:@"Secure"];
            [self.options setValue:@YES forKey:@"Secure"];
        }
        
        // TODO:
        for (NSNumber *number in options) {
            if (number.integerValue == Reconnects) {
                self.reconnects = NO;
            }else if (number.integerValue == ReconnectAttempts){
                reconnectAttempts = -1 ;
            }else if (number.integerValue == ReconnectWait){
                self.reconnectWait = 10;
            }else if (number.integerValue == Nsp){
                self.nsp = @"/";
            }else if (number.integerValue == Log){
                
            }else if (number.integerValue == Logger){
                
            }else if (number.integerValue == HandleQueue){
                handleQueue = dispatch_get_main_queue();
            }else if (number.integerValue == ForceNew){
                self.forceNew = false;
            }
        }
        
        [self.options setValue:@"/socket.io/" forKey:@"Path"];
    }
    return self;
}

- (SocketEngineSpec *)addEngine{
    // TODO:
//    DefaultSocketLogger
    self.engine = [[SocketEngine alloc] initWithClient:self url:self.socketURL options:self.options];
    self.engine.connectParams = self.options[@"connectParams"];;
//  self.engine.socketPath = self.options[@"Path"];
    return _engine;
}

/// Connect to the server.
- (void)connect{
    [self connect:0 complete:nil];
}

/// Connect to the server. If we aren't connected after timeoutAfter, call handler
- (void)connect:(NSInteger)timeoutAfter complete:(CompleteHandler)handler{
    NSAssert(timeoutAfter >= 0, @"Invalid timeout ");
    
    if (self.status == Connected) {
        return;
    }
    
    self.status = Connecting;
    
    if (_engine == nil || self.forceNew) {
        [self addEngine];
        [self.engine connect];
    }else{
        [self.engine connect];
    }
    
    if (timeoutAfter == 0) {
        return;
    }
    
    
    __weak typeof(self) weakSelf = self;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeoutAfter * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        __strong typeof(weakSelf) this = weakSelf;
        if (this.status != Connected && this.status != Disconnected) {
            this.status = Disconnected;
            [this.engine disconnect:@"Connect timeout"];
            if (handler) {
                handler();
            }
        }
    });
}

- (OnAckCallBack)createOnAck:(NSArray *)items{
    currentAck += 1;
    __weak typeof(self) weakSelf = self;
    __block NSInteger ack = currentAck;
    return ^(UInt64 timeout, AckCallback callback){
        __strong typeof(weakSelf) strongSelf = weakSelf;
        [strongSelf->ackHandlers addAck:ack callback:callback];
        [strongSelf _emit:items ack:ack];
        
        if (timeout != 0) {
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeout * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                [ackHandlers timeoutAck:ack];
            });
        }
    };
}

- (void)didConnect{
    // TODO:
//    DefaultSocketLogger
    _status = Connected;
    
    // Don't handle as internal because something crazy could happen where
    // we disconnect before it's handled
    [self handleEvent:@"connect" data:nil isInternalMessage:NO ack:0];
}

- (void)didDisconnect:(NSString *)reason{
    if (_status == Disconnected) {
        return;
    }
    
    // TODO:
//    DefaultSocketLogger
    
    _status = Disconnected;
    
    // Make sure the engine is actually dead.
    [_engine disconnect:reason];
    [self handleEvent:@"disconnect" data:@[reason] isInternalMessage:YES ack:0];
}

/// Disconnects the socket. Only reconnect the same socket if you know what you're doing.
/// Will turn off automatic reconnects.
- (void)disconnect{
//    NSAssert(_status != NotConnected, @"Tried closing a NotConnected client");
  
    // TODO:
//    DefaultSocketLogger
    
    [self didDisconnect:@"Disconnect"];
}

/// Send a message to the server
- (void)emit:(NSString *)event withItems:(NSArray *)items{
    NSString *errorStr = [NSString stringWithFormat:@"Tried emitting %@ when not connected",event];
    if (_status != Connected) {
        @autoreleasepool {
            [self handleEvent:@"error" data:@[errorStr] isInternalMessage:YES ack:0];
        }
        return;
    }
    NSArray *array = @[event];
    if (items != nil) {
      array = [array arrayByAddingObjectsFromArray:items];
    }
  
    [self _emit:array ack:-2];
}

/// Sends a message to the server, requesting an ack. Use the onAck method of SocketAckHandler to add
/// an ack.
- (OnAckCallBack)emitWithAck:(NSString *)event items:(NSArray *)items{
    NSArray *eventArray = @[event];
    if (items != nil) {
      eventArray = [eventArray arrayByAddingObjectsFromArray:items];
    }
    return [self createOnAck:eventArray];
}

- (void)_emit:(NSArray *)data ack:(NSInteger)ack{
  NSLog(@"emit data:%@,ack:%@",data,@(ack));
    dispatch_async(emitQueue, ^{
        if (_status != Connected) {
            [self handleEvent:@"error" data:@[@"Tried emitting when not connected"] isInternalMessage:YES ack:0];
            return;
        }
        
        SocketPacket *packet = [SocketPacket packetFromEmit:data id:(ack < 0 ? -1 : ack) nsp:self.nsp ack:NO];
        NSString *str = packet.packetString;
      NSLog(@"packet.packetString:%@",str);
        // TODO:
//        DefaultSocketLogger
        
        [self.engine send:str datas:packet.binary];
    });
}

// If the server wants to know that the client received data
- (void)emitAck:(NSInteger)ack items:(NSArray *)items{
    dispatch_async(emitQueue, ^{
        if (self.status == Connected) {
            SocketPacket *packet = [SocketPacket packetFromEmit:items id:(ack == 0 ? -1 : ack) nsp:self.nsp ack:YES];
            NSString *str = packet.packetString;
            
            // TODO:
            // DefaultSocketLogger
            
            [self.engine send:str datas:packet.binary];
        }
    });
}

- (void)engineDidClose:(NSString *)reason{
    [waitingPackets removeAllObjects];
    
    if (_status != Disconnected) {
        _status = NotConnected;
    }
    
    if (_status == Disconnected || !_reconnects) {
        [self didDisconnect:reason];
    }else if (!reconnecting){
        reconnecting = YES;
        [self tryReconnectWithReason:reason];
    }
}

/// error
- (void)engineDidError:(NSString *)reason{
    // TODO:
//    DefaultSocketLogger
    [self handleEvent:@"error" data:@[reason] isInternalMessage:YES ack:0];
}

- (void)engineDidOpen:(NSString *)reason{
    // TODO:
//    DefaultSocketLogger
}

// Called when the socket gets an ack for something it sent
- (void)handleAck:(NSInteger)ack data:(NSArray *)data{
    if (_status != Connected) {
        return;
    }
    
    // TODO:
//    DefaultSocketLogger
    
    [ackHandlers executeAck:ack items:data];
}

/// Causes an event to be handled. Only use if you know what you're doing.
// ack 不明确下传0
- (void)handleEvent:(NSString *)event data:(NSArray *)data isInternalMessage:(BOOL)isIntelMsg ack:(NSInteger)ack{
    if (ack == 0) {
        ack = -1;
    }
    
    if (_status == Connected || _status == isIntelMsg) {
        // TODO:
//        DefaultSocketLogger
        dispatch_async(handleQueue, ^{
            if (anyHandler) {
                anyHandler([[SocketAnyEvent alloc] initWith:event items:data]);
            }
            for (SocketEventHandler *handler in handlers) {
                if ([handler.event isEqualToString:event]) {
                    [handler executeCallback:data ack:ack socket:self];
                }
            }
        });
    }else{
        return;
    }
}

/// Leaves nsp and goes back to /
- (void)leaveNamespace{
    if (![self.nsp isEqualToString:@"/"]) {
        [self.engine send:[NSString stringWithFormat:@"1%@",self.nsp] datas:nil];
        self.nsp = @"/";
    }
}

/// Joins namespace
- (void)joinNamespace:(NSString *)Namespace{
    self.nsp = Namespace;
    
    if (![self.nsp isEqualToString:@"/"]) {
        // TODO:
//        DefaultSocketLogger
        [_engine send:[NSString stringWithFormat:@"0%@",self.nsp] datas:nil];
    }
}

/// Removes handler(s) based on name
- (void)offWithEvent:(NSString *)event{
    // TODO: filter需要替换
    NSPredicate *precate = [NSPredicate predicateWithFormat:@"{ $0.event != event }"];
    handlers = [[handlers filteredArrayUsingPredicate:precate] copy];
}

/// Removes a handler with the specified UUID gotten from an `on` or `once`
- (void)offWithId:(NSUUID *)id{
    // TODO: filter需要替换
    NSPredicate *precate = [NSPredicate predicateWithFormat:@"{ $0.id != id }"];
    handlers = [[handlers filteredArrayUsingPredicate:precate] copy];
}

/// Adds a handler for an event.
/// Returns: A unique id for the handler
- (NSUUID *)on:(NSString *)event callback:(NormalCallback)callback{
    // TODO:
//    DefaultSocketLogger
    
    SocketEventHandler *handler = [[SocketEventHandler alloc] init];
    handler.event = event;
    handler.id = [[NSUUID alloc] init];
    handler.callback = callback;
    
    [handlers addObject:handler];
    
    return handler.id;
}

/// Adds a single-use handler for an event.
/// Returns: A unique id for the handler
- (NSUUID *)once:(NSString *)event callback:(NormalCallback)callback{
    // TODO:
    //    DefaultSocketLogger
    
    SocketEventHandler *handler = [[SocketEventHandler alloc] init];
    handler.event = event;
    NSUUID *UUID = [[NSUUID alloc] init];
    handler.id = UUID;
    __weak typeof(self) this = self;
    handler.callback = ^(NSArray *data, SocketAckEmitter *ack){
        [this offWithId:UUID];
        callback(data,ack);
    };
    return handler.id;
}

/// Adds a handler that will be called on every event.
- (void)onAny:(void(^)(SocketAnyEvent *))handler{
    anyHandler = handler;
}

- (void)parseEngineMessage:(NSString *)msg{
    __weak typeof(self) this = self;
    dispatch_async(parseQueue, ^{
        // TODO:
        [this parseSocketMessage:msg];
    });
}

- (void)parseEngineBinaryData:(NSData *)data{
    __weak typeof(self) this = self;
    dispatch_async(parseQueue, ^{
        [this parseBinaryData:data];
    });
}

/// Tries to reconnect to the server.
- (void)reconnect{
    if (reconnecting) {
        return;
    }
    [_engine disconnect:@"manual reconnect"];
}

/// Removes all handlers.
/// Can be used after disconnecting to break any potential remaining retain cycles.
- (void)removeAllHandlers{
    [handlers removeAllObjects];
}

- (void)close {
  [self disconnect];
}
- (void)tryReconnectWithReason:(NSString *)reason{
    if (reconnecting) {
        // TODO:
//        DefaultSocketLogger
        [self handleEvent:@"reconnect" data:@[reason] isInternalMessage:YES ack:0];
        [self _tryReconnect];
    }
}

- (void)_tryReconnect{
    if (!reconnecting) {
        return;
    }
    
    if ((reconnectAttempts != -1 && currentReconnectAttempt + 1 > reconnectAttempts) || !_reconnects) {
        return [self didDisconnect:@"Reconnect Failed"];
    }
    
    // TODO:
//    DefaultSocketLogger
    
    [self handleEvent:@"reconnectAttempt" data:@[@(reconnectAttempts - currentReconnectAttempt)] isInternalMessage:YES ack:0];
    
    currentReconnectAttempt += 1;
    [self connect];
    
    __weak typeof(self) this = self;
    if (_reconnectWait <= 0) {
        _reconnectWait = 10;
    }
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(_reconnectWait * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [this _tryReconnect];
    });
}

#pragma mark - getter setter
- (void)setStatus:(SocketIOClientStatus)status{
    switch (status) {
        case Connected:
            reconnecting = NO;
            currentReconnectAttempt = 0;
            break;
        default:
            break;
    }
}

- (NSString *)sid{
//    return [NSString stringWithFormat:@"%@#%@",self.nsp,self.engine.sid];
  return self.engine.sid;
}

#pragma mark - SocketParsableProtocol methods
//- (void)parseSocketMessage:(NSString *)message{
//
//}

- (void)dealloc{
//    DefaultSocketLogger.Logger.log("Client is being released", type: logType)
    [_engine disconnect:@"Client Deinit"];
}

@end
